Skip to content

feat(client): validate safe integer range for x-mcp-header parameters (#445) - #499

Open
Yudis-bit wants to merge 2 commits into
modelcontextprotocol:mainfrom
Yudis-bit:feat/sep-2243-safe-integer-range
Open

feat(client): validate safe integer range for x-mcp-header parameters (#445)#499
Yudis-bit wants to merge 2 commits into
modelcontextprotocol:mainfrom
Yudis-bit:feat/sep-2243-safe-integer-range

Conversation

@Yudis-bit

@Yudis-bit Yudis-bit commented Sep 9, 2026

Copy link
Copy Markdown

Related to #445.

An out-of-range integer in the ordinary test_custom_headers call can make a client reject that entire call before sending it, incorrectly failing several unrelated header requirements. This change gives the integer-range probe its own tool and places its toolCalls entry last, after the ordinary encoding and null-handling calls.

Changes

  • Declare sep-2243-x-mcp-header-integer-safe-range in sep-2243.yaml and the scenario's emitted check IDs.
  • Advertise test_custom_headers_unsafe_integer with an annotated integer argument and request the exactly representable, out-of-safe-range value 9007199254740992 (2^53). The ordinary tool no longer contains this argument.
  • Fail the range check if the dedicated call sends Mcp-Param-UnsafeInteger; retain the proposed wire-level success result when the requested argument arrives without that header.
  • If the probe never arrives, emit a single range-check failure with details.untestable: true and a diagnostic naming the dedicated tool and possible local rejection. Absence alone cannot distinguish rejection from a skipped call. A missing or changed probe argument also cannot produce a success result.
  • Cover isolation, tool ordering/schema, mirrored and omitted headers, altered/missing arguments, and idempotent backfill in regression tests.
  • Leave the generated traceability manifest to the scheduled workflow; remove the hand-edited coverage entry from this PR.

Specification decision still needed

The discussion on #445 raises both definition-time versus call-time enforcement and error versus omission. This implementation exercises the proposed call-time, wire-level range constraint; it does not settle those questions or establish that silent omission is the normative recovery behavior.

A locally rejecting client still gets an untestable failure for the range check, following the harness's missing-prerequisite policy. Its ordinary header checks remain independently testable. Maintainer guidance is still needed before treating this proposal as the final conformance policy. The rejection validation below is a local fixture, not a C# SDK run.

Validation

  • npm run check — typecheck, ESLint, and Prettier pass.

  • npm run build — passes.

  • npm test45 test files, 595 tests pass, including 15 custom-header tests.

  • Real TypeScript SDK at b65426158ed9f29aea8ef3dc09ca22d7d9d6f970, using its unmodified test/conformance/src/everythingClient.ts, with the SDK packages built from source:

    node dist/index.js client \
      --command "node --import tsx .sdk-under-test/typescript-sdk/test/conformance/src/everythingClient.ts" \
      --scenario http-custom-headers --spec-version 2026-07-28 \
      -o results/pr-499-typescript

    19/19 checks pass; client and runner exit 0. The client used Node 22.14.0 on Windows. Node 24.19.0 produced the same 19 passing wire checks but crashed during client shutdown with a libuv assertion, so it is not counted as a passing end-to-end run.

  • Negative CLI validation with the same SDK and a local fetch wrapper that injects Mcp-Param-UnsafeInteger: 9007199254740992 only for the dedicated probe: 18/19 pass; only the integer-range check fails, runner exit 1.

  • Local-rejection CLI validation with the same SDK and a fetch wrapper that throws before transmitting the dedicated probe: 18/19 pass; only the integer-range check fails, with untestable: true and the dedicated-tool diagnostic; client and runner exit 1.

AI assistance disclosure: Initial implementation used Antigravity; review fixes and validation used Codex.

…modelcontextprotocol#445)

Per the Streamable HTTP specification (SEP-2243) and server/tools.mdx:
'Integer values MUST be within the safe range for integers represented
using IEEE754 double-precision floating point numbers (−2^53+1 to 2^53−1)'

Previously, the conformance harness had no requirement row in sep-2243.yaml,
no emitted check ID, and no verification that clients refrain from mirroring
out-of-range integer values into Mcp-Param headers.

1. Add sep-2243-x-mcp-header-integer-safe-range to src/seps/sep-2243.yaml
   and CUSTOM_HEADERS_DECLARED_CHECK_IDS in http-custom-headers.ts.
2. Add an annotated integer parameter unsafe_integer_val to test_custom_headers
   with context argument 9007199254740992 (2^53).
3. In HttpCustomHeadersScenario.handleToolsCall, verify that the client does
   not mirror unsafe integer arguments into Mcp-Param-UnsafeInteger.
4. Add positive and negative unit test assertions in http-custom-headers.test.ts.
5. Update traceability.json for SEP-2243.

Closes modelcontextprotocol#445
@Danny-Devs

Copy link
Copy Markdown

This will fail csharp-sdk on five checks, and the failure message will name the wrong cause.

The oversized integer goes into the existing test_custom_headers call. csharp-sdk throws while building headers (McpHeaderExtractor.cs#L118), so the call is never sent. With no call arriving, the backfill at http-custom-headers.ts#L238 marks every declared ID it never saw as FAILURE.

Five of the six declared IDs are emitted only from the test_custom_headers branch: supports-custom-headers, mirrors-designated-params, encode-values, base64-unsafe, and the new integer-safe-range. All five go red reporting that the client never called the tool. The real cause is one argument out of seventeen. The sixth, omit-null, comes from the separate test_custom_headers_null call and may survive.

Give the probe its own tool and its own toolCalls entry, placed last. A client that refuses the value then fails one check instead of five.

@JosephDoUrden asked for this issue on August 28 and built the same check, then held it pending a maintainer steer (comment). That comment has the four-SDK survey and the case for whether the constraint binds at definition time or at call time. The dedicated probe tool above is their design.

CONTRIBUTING asks for a run against at least one real SDK before opening a PR. Validation lists typecheck, lint, build and the unit suite, but no SDK run. The unit tests build the request and its headers by hand, so they cannot catch a client that never sends the call.

The bigger question is for the maintainers. Scoring omission as SUCCESS answers the open question in that comment, and makes csharp-sdk non-conforming by merge rather than by decision.

@Yudis-bit

Copy link
Copy Markdown
Author

@Danny-Devs Thanks for catching the coupled failures and pointing me to @JosephDoUrden's earlier design and SDK survey. I've pushed the isolation fix in 7482d37.

  • Moved the unsafe integer into test_custom_headers_unsafe_integer, with its own toolCalls entry placed last. The ordinary encoding and null-handling calls run first.
  • A missing probe now produces only the range-check failure, marked details.untestable: true. Its diagnostic names the dedicated tool and explicitly acknowledges possible local rejection. A missing or changed probe argument cannot pass the check either.
  • Added regression coverage and ran the real TypeScript SDK at b65426158ed9f29aea8ef3dc09ca22d7d9d6f970: 19/19 checks pass, with client and runner exit 0 using Node 22.14.0 on Windows. The PR description includes the command and the Node 24 shutdown limitation.
  • Also ran that SDK through local fetch fixtures: injecting the unsafe header gives 18/19 passing, with only the range check failing; throwing before transmitting the probe gives 18/19 passing, with only the range check marked untestable. The latter simulates local rejection; it is not a C# SDK run.

All 595 tests across 45 files, typecheck, lint, build, and pre-push checks pass.

The specification concern remains open. This revision retains the proposed wire-level success result for omission, and a rejecting client still receives an untestable failure. Isolation fixes the misleading collateral failures; it does not resolve definition-time versus call-time enforcement or error versus omission. I've made that limitation explicit in the code and PR description and removed the description's claim to close #445. Maintainer guidance is still needed before merging this as conformance policy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants